解析器就是個能把 JSON 或 XML 等字串轉為 list、 map 等資料結構,
而這幾天就要用 functional programming 的概念來設計能自由組合的解析器 library,我們的解析器將會從小處著手,先著重辨識輸入中簡單的字元,然後在將這些解析器組合在一起,讓它能解析更複雜的輸入。
我們之前的 API 代數性質,都是從定義資料型態開始,一步步精煉操作用的 function 實現,然後在調整原先的資料型態,其中的定律會在我們知道該如何表達這些資料型態以及 API 如何呈現後產生,
這次的 API 設計我們先來找代數性質以及定律,來看看有什麼不一樣的感覺。
首先我們不應該考慮直接解析如 JSON 或 HTML 之類的文件,好的開始是思考該如何辨識一些無意義的字,所以讓我們從辨識簡單的字元開始吧!
def char(c: Char): Parser[Char]
char function 接受字元做為輸入,然後回傳一個 Parser (解析器),能辨識字元的話,字串也可以如法泡製,
def string(s: String): Parser[String]
執行解析器不應該回傳 ture 或 false 的結果,我們比較想要得到有用的東西,假設我們把 'a'
丟入呼叫 char('a')
所回傳的解析器時,預期會得到 'a'
這個字元,若解析失敗則會回傳失敗的訊息,基於以上說明,我們可以來定義 run function,
def run[A](parser: Parser[A])(input: String): Either[ParseError, A]
這裡可以先不用管 Parser 和 ParseError 怎麼定義或實作,所以我們可以先把這些統整在 trait 底下(也用了 extension 把 run 放在裡面),
trait Parsers[ParseError, Parser[+_]]:
def char(c: Char): Parser[Char]
def string(s: String): Parser[String]
extension[A] (p: Parser[A])
def run(input: String): Either[ParseError, A]
extension 的說明可看 Day 14,
Either 的說明可看 Day 7。
可以看到我們用型態參數來表達 Parser 和 ParseError,而 Parser 的 _
是 high-kinded 型態,代表更高一層的抽象,也就是說把 _
型態參數當作 Parser 的型態建構子,這給了我們在設計 library 時有很大的彈性接收未來可能會用的型態,例如像 Char 那樣 Parser[Char]
;
Parser[+_]
和Parser[+Z]
是一樣的意思,用_
會比較直覺,隱含著所有 Parser 下的所有型態。
在來我們的 char function 應該要滿足以下定律,
char(c).run(c.toString) == Right(c)
同樣的字串也是,
string(s).run(s) == Right(s)
假設我們想辨識字串 "abra" 或 "cadbara" 的話,我們可以來用個 or function 將 2 個 Parser[String] 組合起來,
def or[A](p1: Parser[A], p2: Parser[A]): Parser[A]
然後以下定律可以被滿足,
or(string("abra"), string("cadabra")).run("abra") == Right("abra")
or(string("abra"), string("cadabra")).run("cadabra") == Right("cadabra")
Scala 3 有 infix 這個關鍵字,它能讓我們把 function 當作中綴運算(加減乘除)那樣來使用,所以我們可以在 extension 加入 |
符號代表 or 運算;
trait Parsers[ParseError, Parser[+_]]:
def char(c: Char): Parser[Char]
def string(s: String): Parser[String]
extension[A] (p: Parser[A])
def run(input: String): Either[ParseError, A]
def or(p2: Parser[A]): Parser[A]
infix def |(p2: Parser[A]): Parser[A] = p.or(p2)
最後來考慮一下如何辨識重複字串,
def listOfN[A](n: Int, Parser[A]): Parser[List[A]]
然後我們可以這樣組合使用。
listOfN(3, string("ab") | string("cad")).run("ababcad") == Right("ababcad")
listOfN(3, string("ab") | string("cad")).run("cadabab") == Right("cadabab")
listOfN(3, string("ab") | string("cad")).run("ababab") == Right("ababab")”
好了,到現在我們已經蒐集了一些組合用的 function,但我們還沒有把這些有代數性質的 function 精煉成核心 API,也還沒有最最一般化的定律,我們將在明天繼續做這些事情!。